package net.zhangdaqian.lightDom;

import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Stack;

import org.apache.log4j.Logger;

public class LightParse implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = -7280143242663678577L;
	static Logger logger = Logger.getLogger(LightParse.class.getName());
	private Stack<Element> tagStack = new Stack<Element>();
	private ElementsCollect elementsCollect;
	static String URLAttr = "src href";

	// when the junitTest is false,we replace <br> <p> and some other tag to
	// \n,otherwise not

	private Integer htmlParseIndex = 0;

	private LightDocument lightDocument;

	public LightParse(LightDocument lightDocument) {
		this.lightDocument = lightDocument;
		this.elementsCollect = lightDocument.getElementsCollect();
	}

	private void addElement2TagStack(Element element) {
		element.setElementsCollect(elementsCollect);
		tagStack.add(element);
	}

	private Element popElementFromTagStack() {
		return tagStack.pop();
	}

	private void parseHtmlWithoutTag(StringBuffer html) {
		Element element = new Element();
		element.setTagName("annotation");
		element.setInnerId(lightDocument.getInnerId());
		element.setLevel(1);
		StringBuffer content = new StringBuffer();
		int i = 0;
		while (i < html.length()) {
			content.append(html.charAt(i++));
		}
		element.setContent(content);
		lightDocument.addElement2Collect(element);
	}

	private Element parsePrepareNewElement() {
		Element element = new Element();
		if (tagStack.size() == 0) {
			element.setInnerId(lightDocument.getInnerId());
			element.setLevel(1);
		} else {
			element.setInnerId(lightDocument.getInnerId());
		}
		return element;
	}

	private void parseTagName(StringBuffer html, StringBuffer tagName) {
		Boolean tagNameOK = false;
		while (tagNameOK == false) {
			if (htmlParseIndex < html.length()
					&& html.charAt(htmlParseIndex) != '>'
					&& ((html.charAt(htmlParseIndex) != ' ' || tagName.length() == 0) || (tagName
							.charAt(0) == '/'))) {
				if (html.charAt(htmlParseIndex) != ' ') {
					if (html.charAt(htmlParseIndex) >= 'A'
							&& html.charAt(htmlParseIndex) <= 'Z') {
						tagName.append(Character.toLowerCase(html
								.charAt(htmlParseIndex)));
					} else {
						tagName.append(html.charAt(htmlParseIndex));
					}
				}
			} else {
				tagNameOK = true;
			}
			htmlParseIndex++;
		}
	}

	private void parseAnnotation(StringBuffer html, StringBuffer tagName) {
		int commentStart = htmlParseIndex - tagName.length() + "!--".length()
				- 1;
		int commentEnd;
		// if this is the ie only trick,we only can do ishope the designer can
		// do the right tag ,but if they not ,we get into the bug mode
		if (tagName.indexOf("[if") > 0) {
			if (html.indexOf("-->", htmlParseIndex - tagName.length()) > 0) {
				// int shor
				commentEnd = html.indexOf("-->", htmlParseIndex
						- tagName.length());
				String annotation = html.substring(commentStart, commentEnd);
				// we just skip these if,we do not what it is,it has nothing to
				// do with the tinydom
				htmlParseIndex = (htmlParseIndex - 1)
						- (tagName.length() - "!--".length())
						+ annotation.length() + "-->".length();
			} else
			// bug mode
			{
				commentEnd = html.indexOf("endif", htmlParseIndex
						- tagName.length());
				String annotation = html.substring(commentStart, commentEnd);
				// we just skip these if,we do not what it is,it has nothing to
				// do with the tinydom
				while (commentEnd < html.length()
						&& html.charAt(commentEnd++) != '>') {

				}
				htmlParseIndex = commentEnd;
			}
		} else {
			commentEnd = html.indexOf("-->", htmlParseIndex - tagName.length());
			String annotation = html.substring(commentStart, commentEnd);
			htmlParseIndex = (htmlParseIndex - 1)
					- (tagName.length() - "!--".length()) + annotation.length()
					+ "-->".length();
			logger.debug("annotation: " + annotation);
			LightDocument addition = new LightDocument();
			addition.setUrl(lightDocument.getUrl().getURI());
			mergeDocument(tagStack.peek(), lightDocument, addition
					.parse(annotation));
		}
	}

	private void parseDTD(StringBuffer html, StringBuffer tagName) {
		if (tagName.toString().toLowerCase().equals("!doctype")) {
			lightDocument.dtd.append(tagName + " ");
			while (html.charAt(htmlParseIndex) != '>') {
				lightDocument.dtd.append(html.charAt(htmlParseIndex++));
			}
		}
	}

	private void parseStartTag(StringBuffer html, Element element) {
		if (tagStack.size() == 0) {
			addElement2TagStack(element);
		} else {
			addElement2TagStack(setElement(tagStack.peek(), element));
		}
		// get tag's attributes,which are key value pair
		parseAttributes(tagStack.peek(), html.substring(htmlParseIndex - 1, html
				.indexOf(">", htmlParseIndex - 1)));
		htmlParseIndex = html.indexOf(">", htmlParseIndex - 1) + ">".length();
	}

//	private void getAttributes(Element element, String attr) {
//		if (attr.length() > 0) {
//			element.parseAttributes(attr);
//		}
//	}

	public void parseAttributes(Element element, String attr) {
		// if a attributes contains "/" mean this is a close tag.this mark
		// can
		// appear in 2 ways
		// 1.<XXX YY=ZZ DD=EE />
		// 2.<meta http-equiv="Content-Type"
		// content="text/html; charset=UTF-8" /> the mark / also can in
		// the middle
		LinkedList<String> attr_keys = new LinkedList<String>();
		LinkedList<String> attr_values = new LinkedList<String>();
		if (attr.length() > 0) {
			attr = attr.trim();

			for (int j = attr.length() - 1; j >= 0; j--) {
				if (attr.charAt(j) == ' ' || attr.charAt(j) == '/') {
					if (attr.charAt(j) == '/') {
						element.setEndTag(true);
						break;
					}
				} else {
					break;
				}
			}

			nextAttr:

			for (int i = 0; i < attr.length(); i++) {
				StringBuffer key = new StringBuffer();
				StringBuffer value = new StringBuffer();
				while (i < attr.length() && attr.charAt(i) != '='
						&& (attr.charAt(i) != ' ' || key.length() == 0)) {
					if (attr.charAt(i) != ' ')
						key.append(attr.charAt(i));
					i++;
				}
				if (i == attr.length()) {
					break nextAttr;
				}
				while (i < attr.length() && attr.charAt(i) != '=') {
					if (attr.charAt(i) != ' ') {
						i--;
						attr_keys.addLast(key.toString());
						attr_values.addLast(key.toString());
						continue nextAttr;
					} else {
						i++;
					}
				}
				// if here,charAt i is "=",and skip this
				i++;
				Boolean hasMarkes = true;
				char marke = ' ';
				while (i < attr.length() && hasMarkes == true) {
					if (i < attr.length() && attr.charAt(i) != '\''
							&& attr.charAt(i) != '\"' && attr.charAt(i) != ' ') {
						hasMarkes = false;
					} else {
						if (i < attr.length() && attr.charAt(i) != ' ') {
							marke = attr.charAt(i);
							break;
						}
						i++;
					}
				}
				// ZhDaqian Bug when there is \' or \" this may not work well
				if (hasMarkes) {
					i++;
					while (i < attr.length() && (attr.charAt(i) != marke)) {
						value.append(attr.charAt(i++));
					}
				} else {
					while (i < attr.length() && attr.charAt(i) != ' ') {
						value.append(attr.charAt(i++));
					}
				}
				String _key = key.toString().trim();
				String _value = value.toString().trim();
				if (URLAttr.indexOf(_key) >= 0) {
					_value = this.elementsCollect.getDocument().getUrl().merge(
							_value);
				}
				attr_keys.addLast(_key);
				attr_values.addLast(_value);
				if (key.equals("id"))
					element.setId(_value);
				if (key.equals("name"))
					element.setName(_value);
			}
			element.setAttribute(attr_keys, attr_values);
		}

	}

	private void parseCloseTag(StringBuffer html, StringBuffer tagName) {
		Boolean findMatch = false;
		String tag = (tagName.substring(tagName.indexOf("/") + 1, tagName
				.length())).trim();
		while (tagStack.size() > 0 && findMatch == false) {
			// very well these tag match perfect
			if (tagStack.peek().getTagName().equals(tag)) {
				findMatch = true;
				lightDocument.addElement2Collect(popElementFromTagStack());
			}
			// ooops....here we deal
			// 1.if the tag is a div,this means it should find a div
			// to pop out ,and if the tag is a <br> we pop <br>out
			// and go on untile we find what we want.
			// 2.but also a div can't pop a body tag out,so we
			// compare they weight.
			// 3.and if the stack is only one element,what's this
			// mean?this means this is the root element,and if it
			// does not match the perfect one ,can't be pop out for
			// ever .
			else {
				logger.debug(tagStack.peek().getTagName() + "<--peek   now--->"
						+ tag);
				if (tagStack.size() > 1
						&& getHtmlTagWeight(tagStack.peek().getTagName()) <= getHtmlTagWeight(tag)) {
					lightDocument.addElement2Collect(popElementFromTagStack());
				} else {
					findMatch = true;
				}
			}
		}

	}

	private void parseTag(StringBuffer html, StringBuffer tagName,
			StringBuffer content, Element element) {
		// here we get with the tagname,of course any tag name's length
		// should more than zero.and a tag name should not have a blank
		// character or a '>'!
		parseTagName(html, tagName);
		element.setTagName(tagName.toString());
		// here we deal with the tagname
		// 1.if a tag name has "/" it should be a end tag ,not a start
		if (tagName.indexOf("/") >= 0 && tagName.indexOf("!") < 0) {
			element.setEndTag(true);
		}
		// 2.if a tag start with "!" we take it as a lightDocument type
		// definition or a annotation
		else if (tagName.indexOf("!") >= 0) {
			// take as annotation
			if (tagName.indexOf("!--") == 0) {
				parseAnnotation(html, tagName);
			}
			// take as lightDocument type definition
			else if (tagName.indexOf("!") == 0) {
				parseDTD(html, tagName);
			}
			// if a tag start with ! and not both of these above,we log
			// it!
			else {
				logger.debug(" a tag name with !  don't konw how to to"
						+ tagName);
			}
		}
		// if a tag does not have "!" or "/" ,we assume it is a start
		// element
		else {
			// add this element to stack
			parseStartTag(html, element);
		}
		// and if this is a close tag ,here find the suitable tag in tag
		// stack and pop them out
		if (element.isEndTag()) {
			parseCloseTag(html, tagName);
			// if this is a block element ,we put a \n.if you don't do
			// this,no harm.just for a beautiful look.but there is
			// switch call junitTest.if junitTest equals true,skip this
			if (isBlockElement((tagName.substring(tagName.indexOf("/") + 1,
					tagName.length())).trim())) {
				content.append("\n");
			}

		}
	}

	private void parseContent(StringBuffer html, StringBuffer content) {
		while (htmlParseIndex < html.length()
				&& html.charAt(htmlParseIndex) != '<') {
			content.append(html.charAt(htmlParseIndex++));
		}
		// append the content to the peek of tag stack
		try {
			if (htmlParseIndex < html.length()) {
				tagStack.peek().setContent(content);
				if (tagStack.peek().getTagName().length() == 5
						&& tagStack.peek().getTagName().equals("title")) {
					lightDocument.title = content.toString();
				}
			}
		} catch (Exception e) {
			logger.debug("when set content:" + content);
		}
	}

	public LightDocument parse(StringBuffer html) {
		htmlParseIndex = html.indexOf("<");
		// first we check if this is a html or any way a xml file.if it is ,it
		// should hava a '<' this is where we start the parse program,otherwise
		// we assume that it is a annotation.
		if (htmlParseIndex < 0) {
			parseHtmlWithoutTag(html);
		} else {
			// if it's not a annotation ,we deal it with a element or tag
			// (<XXXX> YYY </XXXX>) each time.
			for (; ++htmlParseIndex < html.length();) {
				Element element = parsePrepareNewElement();
				StringBuffer tagName = new StringBuffer();
				StringBuffer content = new StringBuffer();
				// and here we deal with tag , tag are those you can't see
				// them in a web browse
				parseTag(html, tagName, content, element);
				// and here we deal with content , content are those you can see
				// them in a web browse
				parseContent(html, content);
			}
		}
		// at last the tag stack should be zero,if not oops....
		{
			if (tagStack.size() != 0) {
				logger.debug("%%%%%%%%%%%%%%%%%%%%%");
				logger.debug("Stack Error:\n" + html);
				logger.debug("%%%%%%%%%%%%%%%%%%%%%");
			}
			while (tagStack.size() != 0) {
				lightDocument.addElement2Collect(popElementFromTagStack());
			}
		}
		return lightDocument;
	}

	private Boolean mergeDocument(Element sourceElement, LightDocument source,
			LightDocument addition) {
		try {
			addition.getElementsCollect().sortByInnerId();
			for (Iterator<Element> iterator = addition.getElementsCollect().elements
					.iterator(); iterator.hasNext();) {
				Element additionElement = (Element) iterator.next();
				additionElement.setInnerId(lightDocument.getInnerId());
				additionElement.setLevel(additionElement.getLevel()
						+ sourceElement.getLevel());
				if (additionElement.getParent() == null) {
					sourceElement.setChildren(additionElement);
					additionElement.setParent(sourceElement);
				}
				source.addElement2Collect(additionElement);
			}
		} catch (Exception e) {
			logger.debug("this should not happen\n when mergeDocument ");
		}
		return true;
	}

	private Integer getHtmlTagWeight(String tag) {
		Integer weight = -1;
		if (tag.equals("div")) {
			weight = 800;
		} else if (tag.equals("body")) {
			weight = 900;
		} else if (tag.equals("html")) {
			weight = 1000;
		}
		return weight;
	}

	private Boolean isBlockElement(String tag) {
		if (lightDocument.getJunitTest()) {
			return false;
		}
		return "div td table p br".indexOf(tag) >= 0;
	}

	private Element setElement(Element peek, Element element) {
		// TODO Auto-generated method stub
		element.setLevel(peek.getLevel() + 1);
		element.setParent(peek);
		peek.setChildren(element);
		return element;
	}
}
